/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { ArkFile, ts } from 'arkanalyzer';

import { BaseChecker, BaseMetaData } from '../BaseChecker';
import { Defects } from '../../Index';
import { AstMatcher, MatcherCallback, MatcherTypes } from '../../Index';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { Rule } from '../../Index';
import { RuleListUtil } from '../../utils/common/DefectsList';
import { IssueReport } from '../../model/Defects';
import { RuleFix } from '../../model/Fix';
import { CheckerUtils } from '../../utils/checker/CheckerUtils';


const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'ConsistentTypeImportsCheck');

const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: 'docs/consistent-type-imports.md',
    description: 'Enforce consistent usage of type imports.'
};

enum TypeImportsFixStyle {
    inline = 0,
    separate = 1
}

enum TypeImportsPrefer {
    noType = 0,
    type = 1
}

type RuleOptions = {
    disallowTypeAnnotations: boolean,
    fixStyle: TypeImportsFixStyle,
    prefer: TypeImportsPrefer
}

type Options = {
    disallowTypeAnnotations: boolean,
    fixStyle: string,
    prefer: string
}

type CheckMember = {
    node: ts.Node,
    name: string,
    used: boolean
}

enum ImportUseType {
    all = 0,
    some = 1,
    none = 2
}

export class ConsistentTypeImportsCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public issues: IssueReport[] = [];

    private typeMessage = 'All imports in the declaration are only used as types. Use `import type`.';
    private noTypeMessage = 'Use an `import` instead of an `import type`.';
    private disallowTypeAnnotationsMessage = '`import()` type annotations are forbidden.';
    private ruleOptions: RuleOptions = { disallowTypeAnnotations: true, fixStyle: TypeImportsFixStyle.separate, prefer: TypeImportsPrefer.type }
    private usedNodeList: CheckMember[] = [];
    private importTypeNodeList: string[] = [];

    private fileTypeUsageMap: Map<string, CheckMember[]> = new Map();
    private fileImportTypeMap: Map<string, string[]> = new Map();
    private initializedFiles: Set<string> = new Set();

    // Simplified AST Matchers using factory method
    private createMatcher = (predicate: (node: ts.Node) => boolean): AstMatcher => ({
        matcherType: MatcherTypes.AST,
        match: predicate
    });

    private readonly matchers = {
        importDeclaration: this.createMatcher(ts.isImportDeclaration),
        typeAliasDeclaration: this.createMatcher(ts.isTypeAliasDeclaration),
        variableStatement: this.createMatcher(ts.isVariableStatement),
        typeReferenceNode: this.createMatcher(ts.isTypeReferenceNode),
        typeQueryNode: this.createMatcher(ts.isTypeQueryNode)
    };

    public registerMatchers(): MatcherCallback[] {
        return [
            { matcher: this.matchers.importDeclaration, callback: this.checkImportDeclaration },
            { matcher: this.matchers.typeAliasDeclaration, callback: this.checkTypeAliasDeclaration },
            { matcher: this.matchers.variableStatement, callback: this.checkVariableStatementNode },
            { matcher: this.matchers.typeReferenceNode, callback: this.checkTypeReferenceNode },
            { matcher: this.matchers.typeQueryNode, callback: this.checkTypeQueryNode }
        ];
    }

    // Required by BaseChecker interface
    public check = (_targetFile: ArkFile): void => {
        // Initialize rule options
        this.initializeRuleOptions();
    };

    private initializeRuleOptions(): void {
        if (this.rule.option.length === 0) {
            return;
        }

        const option = this.rule.option[0] as Options;
        const fixStyleMap: Record<string, TypeImportsFixStyle> = {
            'inline-type-imports': TypeImportsFixStyle.inline,
            'separate-type-imports': TypeImportsFixStyle.separate
        };
        const preferMap: Record<string, TypeImportsPrefer> = {
            'no-type-imports': TypeImportsPrefer.noType,
            'type-imports': TypeImportsPrefer.type
        };

        this.ruleOptions = {
            disallowTypeAnnotations: option.disallowTypeAnnotations ?? this.ruleOptions.disallowTypeAnnotations,
            fixStyle: fixStyleMap[option.fixStyle] ?? this.ruleOptions.fixStyle,
            prefer: preferMap[option.prefer] ?? this.ruleOptions.prefer
        };
    }



    // Unified method to handle all common checks and initialization
    private prepareFileForProcessing(targetFile: ArkFile, requiresTypeUsage: boolean = true): boolean {
        // Check if file should be processed using the common utility
        if (!CheckerUtils.shouldProcessFile(targetFile, ['.ts'])) {
            return false;
        }

        // Initialize file if type usage information is required
        if (requiresTypeUsage) {
            return this.initializeFileIfNeeded(targetFile);
        }

        return true;
    }



    // Unified method for processing type usage nodes
    private processTypeUsageNode(nameNode: ts.EntityName, nodeType: string, node: ts.Node, targetFile: ArkFile): void {
        // Inline AstMatcherUtils.processTypeUsageNode
        const typeName = this.extractTypeName(nameNode);
        if (typeName) {
            this.saveEntityNameNodesForFile(nameNode, targetFile.getFilePath());
        }
    }

    // Unified method for processing nodes that require source file
    private processNodeWithSourceFile(
        node: ts.Node,
        targetFile: ArkFile,
        processor: (sourceFile: ts.SourceFile, node: ts.Node, targetFile: ArkFile) => void
    ): void {
        const result = CheckerUtils.processNodeWithSourceFile(node, targetFile, processor);
        if (result === null) {
            // 如果处理失败，抛出错误以保持原有行为
            throw new Error('Failed to process node with source file');
        }
    }

    // Unified method for calculating node position and reporting issues
    private reportIssueAtNode(
        sourceFile: ts.SourceFileLike,
        node: ts.Node,
        targetFile: ArkFile,
        message: string,
        fix?: RuleFix,
        columnOffset: number = 0
    ): void {
        const position = CheckerUtils.getNodePosition(sourceFile, node);
        this.addIssueReport(targetFile, position.line, position.column + columnOffset, 0, message, fix);
    }

    // Unified file initialization method - ensures type usage is collected only once per file
    private initializeFileIfNeeded(targetFile: ArkFile): boolean {
        const filePath = targetFile.getFilePath();

        if (this.initializedFiles.has(filePath)) {
            // File already initialized, just set the current context
            this.usedNodeList = this.fileTypeUsageMap.get(filePath) || [];
            this.importTypeNodeList = this.fileImportTypeMap.get(filePath) || [];
            return true;
        }

        // First time processing this file - collect type usage information
        this.usedNodeList = [];
        this.importTypeNodeList = [];

        const sourceFile = CheckerUtils.getSourceFileFromArkFile(targetFile);
        if (!sourceFile) {
            return false;
        }

        const sourceFileObject = CheckerUtils.getParseTreeNode(sourceFile);
        if (!sourceFileObject) {
            return false;
        }

        this.loopNodeForIssue(targetFile, sourceFile, sourceFileObject);

        // Cache the collected data
        this.fileTypeUsageMap.set(filePath, [...this.usedNodeList]);
        this.fileImportTypeMap.set(filePath, [...this.importTypeNodeList]);
        this.initializedFiles.add(filePath);

        return true;
    }

    // Generic callback creator to reduce repetition
    private createCallback = (
        processor: (sourceFile: ts.SourceFile, node: ts.Node, targetFile: ArkFile) => void,
        requiresTypeUsage: boolean = true
    ) => (node: ts.Node, targetFile: ArkFile): void => {
        if (!this.prepareFileForProcessing(targetFile, requiresTypeUsage)) {
            return;
        }
        this.processNodeWithSourceFile(node, targetFile, processor);
    };

    // AST Matcher callback methods
    public checkImportDeclaration = this.createCallback(this.checkImport.bind(this));
    public checkTypeAliasDeclaration = this.createCallback(this.checkTypeAlias.bind(this));
    public checkVariableStatementNode = this.createCallback(this.checkVariableStatement.bind(this));

    public checkTypeReferenceNode = (node: ts.Node, targetFile: ArkFile): void => {
        if (!this.prepareFileForProcessing(targetFile, false)) {
            return;
        }
        if (ts.isTypeReferenceNode(node)) {
            this.processTypeUsageNode(node.typeName, 'type reference', node, targetFile);
        }
    };

    public checkTypeQueryNode = (node: ts.Node, targetFile: ArkFile): void => {
        if (!this.prepareFileForProcessing(targetFile, false)) {
            return;
        }
        if (ts.isTypeQueryNode(node)) {
            this.processTypeUsageNode(node.exprName, 'type query', node, targetFile);
        }
    };

    private checkImport(sourceFile: ts.SourceFileLike, node: ts.Node, targetFile: ArkFile): void {
        const nodeList = node.getChildren();
        if (nodeList.length < 3 || this.containsAssert(nodeList)) {
            return;
        };
        const second = nodeList[1];
        if (!ts.isImportClause(second)) {
            return;
        };
        const secondNodeList = second.getChildren();
        if (secondNodeList.length < 1) {
            return;
        };
        const secondFirst = secondNodeList[0];
        const hasType = secondFirst.kind === ts.SyntaxKind.TypeKeyword;
        if ((this.ruleOptions.prefer === TypeImportsPrefer.noType && hasType) ||
            (this.ruleOptions.prefer === TypeImportsPrefer.type && !hasType)) {
            const { message, fix } = this.checkImportPreference(hasType, sourceFile, nodeList, second, secondFirst, secondNodeList, node);
            if (message) {
                this.reportIssueAtNode(sourceFile, node, targetFile, message, fix);
            };
        };
    };
    private checkImportPreference(
        hasType: boolean,
        sourceFile: ts.SourceFileLike,
        nodeList: ts.Node[],
        second: ts.ImportClause,
        secondFirst: ts.Node,
        secondNodeList: ts.Node[],
        node: ts.Node): { message: string | undefined; fix: RuleFix | undefined } {
        let message;
        let fix: RuleFix | undefined;
        const first = nodeList[0];
        if (this.ruleOptions.prefer === TypeImportsPrefer.noType && hasType) {
            message = this.noTypeMessage;
            fix = { range: [first.getStart(), secondFirst.getEnd()], text: first.getText() };
        } else {
            const msg = this.getImportMessage(secondNodeList);
            if (msg) {
                message = msg;
            } else {
                return { message: undefined, fix: undefined };
            };
            const secondText = second.getText();
            if (secondText.charAt(0) === '{' && this.ruleOptions.fixStyle === TypeImportsFixStyle.inline) {
                const nodePosition = CheckerUtils.getNodePosition(sourceFile, second);
                const position = ts.getPositionOfLineAndCharacter(sourceFile, nodePosition.line - 1, nodePosition.column);
                fix = { range: [second.getStart(), position], text: '{ type ' };
            } else if (secondText.includes(',') && secondText.charAt(0) !== '{') {
                fix = this.handleComplexImport(nodeList, secondText, node);
            } else if (secondText.startsWith('{') && secondText.endsWith('}') && this.importTypeNodeList.length > 0) {
                fix = this.handleNamedImports(nodeList, secondText, node);
            } else {
                fix = { range: [first.getStart(), first.getEnd()], text: first.getText() + ' type' };
            };
        };
        return { message, fix };
    };

    private processImports(items: string[], sourceText: string): string {
        const typeImports: string[] = [];
        const valueImports: string[] = [];
        for (const item of items) {
            if (this.importTypeNodeList.includes(item)) {
                typeImports.push(item);
            } else {
                valueImports.push(item);
            };
        };
        let newText = '';
        if (typeImports.length > 0) {
            newText += `import type { ${typeImports.join(', ')} } from ${sourceText};\n`;
        };
        if (valueImports.length > 0) {
            newText += `import { ${valueImports.join(', ')} } from ${sourceText};`;
        } else if (typeImports.length > 0) {
            newText = newText.substring(0, newText.length - 1);
        };
        return newText;
    };

    //处理 import A3, { B3 } from 'foo' 情况的方法
    private handleComplexImport(nodeList: ts.Node[], secondText: string, node: ts.Node): RuleFix | undefined {
        const importSourceNode = nodeList[3];
        const sourceText = importSourceNode.getText();
        const commaIndex = secondText.indexOf(',');
        const defaultImport = secondText.substring(0, commaIndex).trim();
        const namedImports = secondText.substring(commaIndex + 1).trim();
        let newText = '';
        if (namedImports) {
            const isEmptyObject = namedImports === '{}' || namedImports.match(/^\{\s*\}$/);
            if (!isEmptyObject) {
                const importItems = namedImports.substring(1, namedImports.length - 1)
                    .split(',')
                    .map(item => item.trim());
                if (this.importTypeNodeList.length > 0) {
                    newText += this.processImports(importItems, sourceText);
                } else {
                    newText += `import type ${namedImports} from ${sourceText};`;
                };
            };
        };
        if (defaultImport) {
            if (newText && !newText.endsWith('\n')) {
                newText += '\n';
            };
            newText += `import type ${defaultImport} from ${sourceText};`;
        };
        return { range: [node.getStart(), node.getEnd()], text: newText };
    };

    private handleNamedImports(nodeList: ts.Node[], secondText: string, node: ts.Node): RuleFix {
        const importSourceNode = nodeList[3];
        const sourceText = importSourceNode.getText();
        const namedImportText = secondText.trim();
        // 去掉括号获取实际的导入项列表
        const importItems = namedImportText.substring(1, namedImportText.length - 1)
            .split(',')
            .map(item => item.trim());
        const newText = this.processImports(importItems, sourceText);
        return { range: [node.getStart(), node.getEnd()], text: newText };
    };

    // 语句中是否包含assert，例如 import * as Type4 from 'foo' assert { type: 'json' };
    private containsAssert(nodeList: ts.Node[]): boolean {
        for (const node of nodeList) {
            if (ts.isAssertClause(node)) {
                return true;
            }
        }
        return false;
    }

    private getImportMessage(nodeList: ts.Node[]): string | undefined {
        let message: string | undefined;
        let importNameNodes: (ts.NamedImports | ts.Identifier)[] = [];
        for (const importNameNode of nodeList) {
            const secondFirst = importNameNode;
            const namespaceChildren = secondFirst.getChildren();
            if (ts.isNamespaceImport(secondFirst) && namespaceChildren.length >= 2) { // import * as A2 from 'foo'; 中的 * as A2
                const lastNode = namespaceChildren[namespaceChildren.length - 1];
                if (namespaceChildren[namespaceChildren.length - 2].kind === ts.SyntaxKind.AsKeyword && ts.isIdentifier(lastNode)) {
                    importNameNodes.push(lastNode);
                }
            } else if (ts.isNamedImports(secondFirst) || ts.isIdentifier(secondFirst)) {
                importNameNodes.push(secondFirst);
            }
        }

        if (importNameNodes.length === 0) {
            return undefined;
        }
        const used = this.getImportNodeUsed(importNameNodes);
        if (used === ImportUseType.all) {
            message = this.typeMessage;
        } else if (used === ImportUseType.some) {
            const names = this.getUsedImportNames(importNameNodes);
            message = names + ' only used as types.';
        } else if (used === ImportUseType.none) {
            return undefined;
        }
        return message;
    }

    // 通用的 import type 检查方法
    private checkImportTypeNode(sourceFile: ts.SourceFileLike, importTypeNode: ts.Node, targetFile: ArkFile): void {
        if (!this.ruleOptions.disallowTypeAnnotations) {
            return;
        }

        // 如下代码开始列需要去掉typeof: typeof import('foo')
        const hasTypeOfKeyword = importTypeNode.getChildren().length > 0 &&
            importTypeNode.getChildren()[0].kind === ts.SyntaxKind.TypeOfKeyword;
        const columnOffset = hasTypeOfKeyword ? 7 : 0;

        this.reportIssueAtNode(sourceFile, importTypeNode, targetFile, this.disallowTypeAnnotationsMessage, undefined, columnOffset);
    }

    private checkTypeAlias(sourceFile: ts.SourceFileLike, node: ts.Node, targetFile: ArkFile): void {
        const nodeList = node.getChildren();
        for (const child of nodeList) {
            let object = child;
            if (ts.isUnionTypeNode(child)) {
                const foundNode = this.getSpecifyChild(child, ts.SyntaxKind.ImportType);
                if (foundNode) {
                    object = foundNode;
                }
            }
            if (ts.isImportTypeNode(object)) {
                this.checkImportTypeNode(sourceFile, object, targetFile);
                break;
            }
        }
    }

    private getSpecifyChild(aNode: ts.Node, kind: ts.SyntaxKind): ts.Node | undefined {
        for (const child of aNode.getChildren()) {
            if (child.kind === kind) {
                return child;
            }
            let result = this.getSpecifyChild(child, kind);
            if (result) {
                return result;
            }
        }
        return undefined;
    }

    private checkVariableStatement(sourceFile: ts.SourceFileLike, node: ts.Node, targetFile: ArkFile): void {
        const nodeList = node.getChildren();
        for (const child of nodeList) {
            if (ts.isImportTypeNode(child)) {
                this.checkImportTypeNode(sourceFile, child, targetFile);
                break;
            } else {
                this.checkVariableStatement(sourceFile, child, targetFile);
            }
        }
    }

    private addIssueReport(arkFile: ArkFile, line: number, startCol: number, endCol: number, message: string, fix: RuleFix | undefined): void {
        const severity = this.rule.alert ?? this.metaData.severity;
        const defect = new Defects(line, startCol, endCol, message, severity, this.rule.ruleId,
            arkFile.getFilePath(), this.metaData.ruleDocPath, true, false, true);
        this.issues.push(new IssueReport(defect, fix));
        RuleListUtil.push(defect);
    }

    private loopNodeForIssue(targetFile: ArkFile, sourceFile: ts.SourceFileLike, aNode: ts.Node): void {
        try {
            const children = aNode.getChildren();
            for (const child of children) {
                if (ts.isTypeReferenceNode(child)) {
                    this.saveEntityNameNodes(child.typeName);
                } else if (ts.isTypeQueryNode(child)) {
                    this.saveEntityNameNodes(child.exprName);
                } else {
                    this.loopNodeForIssue(targetFile, sourceFile, child);
                }
            }
        } catch (error) {
            logger.error('Error in loopNodeForIssue', {
                filePath: targetFile.getFilePath(),
                nodeKind: ts.SyntaxKind[aNode.kind],
                error: error instanceof Error ? error.message : String(error)
            });
            throw error;
        }
    }

    // 提取类型名的通用方法
    private extractTypeName(nameNode: ts.EntityName): string | undefined {
        // Inline AstMatcherUtils.extractEntityName
        if (ts.isIdentifier(nameNode)) {
            return nameNode.getText();
        } else if (ts.isQualifiedName(nameNode)) {
            return nameNode.left.getText();
        }
        return undefined;
    }

    // 保存文件内所有使用了的类的节点
    private saveEntityNameNodes(nameNode: ts.EntityName): void {
        const typeName = this.extractTypeName(nameNode);
        if (typeName) {
            this.usedNodeList.push({ node: nameNode, name: typeName, used: false });
        }
    }

    // import的节点被使用的情况
    private getImportNodeUsed(nodes: (ts.NamedImports | ts.Identifier)[]): ImportUseType {
        const importNodes = this.getObjectsInImport(nodes);
        const usedCount = importNodes.filter(node => node.used).length;

        if (usedCount === importNodes.length) {
            return ImportUseType.all;
        }
        if (usedCount > 0) {
            return ImportUseType.some;
        }
        return ImportUseType.none;
    }

    // 获取import内的节点
    private getObjectsInImport(nodes: (ts.NamedImports | ts.Identifier)[]): CheckMember[] {
        const nodeList: ts.Identifier[] = [];

        for (const aNode of nodes) {
            if (ts.isNamedImports(aNode)) {
                nodeList.push(...aNode.elements.map(element => element.name));
            } else if (ts.isIdentifier(aNode)) {
                nodeList.push(aNode);
            }
        }

        const usedNames = new Set(this.usedNodeList.map(obj => obj.name));
        return nodeList.map(node => {
            const name = node.getText();
            return { node, name, used: usedNames.has(name) };
        });
    }

    // 获取所有使用了的import节点名称
    private getUsedImportNames(nodes: (ts.NamedImports | ts.Identifier)[]): string {
        const importNodes = this.getObjectsInImport(nodes);
        const nameList = importNodes.filter(node => node.used).map(node => {
            this.importTypeNodeList.push(node.name);
            return node.name;
        });

        if (nameList.length === 0) {
            return '';
        }
        if (nameList.length === 1) {
            return `Import "${nameList[0]}" is`;
        }

        const lastIndex = nameList.length - 1;
        const allButLast = nameList.slice(0, lastIndex).map(name => `"${name}"`).join(', ');
        return `Imports ${allButLast} and "${nameList[lastIndex]}" are`;
    }



    private saveEntityNameNodesForFile(nameNode: ts.EntityName, filePath: string): void {
        const typeName = this.extractTypeName(nameNode);
        if (typeName) {
            if (!this.fileTypeUsageMap.has(filePath)) {
                this.fileTypeUsageMap.set(filePath, []);
            }
            this.fileTypeUsageMap.get(filePath)!.push({ node: nameNode, name: typeName, used: false });
        }
    }
}